﻿// ============================================================================
//  _ __   ___  ___  ___ _ __ ___  _   _
// | '_ \ / _ \/ __|/ _ \ '_ ` _ \| | | |
// | | | |  __/\__ \  __/ | | | | | |_| |
// |_| |_|\___||___/\___|_| |_| |_|\__,_|
//
// NES Emulator by daxnet, 2024
// MIT License
// ============================================================================

using System.Text;
using FontStashSharp;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MonoGame.Forms.NET.Controls;
using NesEmu.Console.Models;
using NesEmu.Console.Palettes;
using NesEmu.Core;
using Color = Microsoft.Xna.Framework.Color;
using Rectangle = Microsoft.Xna.Framework.Rectangle;

namespace NesEmu.Console;

internal class GameControl : MonoGameControl
{
    #region Public Enums

    public enum ViewMode
    {
        FullWindow = 0,
        Normal = 1,
        Scaled = 2,
        Debug = 3
    }

    #endregion Public Enums

    #region Public Constructors

    public GameControl()
    {
        _emulator.DrawFrame += _emulator_DrawFrame;
        _emulator.CpuStepped += _emulator_CpuStepped;
        AlwaysEnableKeyboardInput = true;

        SelectedColorPalette = ColorPalette.GetColorPaletteByName("Default");
        SelectedControllerSetting = ControllerSetting.Default;
    }

    #endregion Public Constructors

    #region Private Fields

    private const string CaptionText = "NES EMULATOR FOR .NET";

    private static readonly Color[] TextColors =
    [
        new Color(236, 238, 236),
        new Color(76, 154, 236),
        new Color(120, 124, 236),
        new Color(176, 98, 236),
        new Color(228, 84, 236),
        new Color(236, 88, 180),
        new Color(236, 106, 100),
        new Color(212, 136, 32),
        new Color(160, 170, 0),
        new Color(116, 196, 0),
        new Color(76, 208, 32),
        new Color(56, 204, 108),
        new Color(56, 180, 204)
    ];

    private readonly FontSystem _captionFontSystem = new();

    private readonly Color[] _colors = new Color[256 * 240];

    private readonly FontSystem _dbgFontSystem = new();

    private readonly Emulator _emulator = new();

    private readonly Input _input = new();

    private readonly Queue<string> _instructionQueue = new();

    private Color _backgroundColor = Color.Black;

    private DynamicSpriteFont? _captionFont;

    private Vector2 _captionTextSize;

    private DynamicSpriteFont? _dbgFont;

    private int _dbgFontHeight;

    private DrawFrameEventArgs? _drawFrameEventArgs;

    private Thread? _gameThread;

    private Texture2D? _texture;

    #endregion Private Fields

    #region Public Events

    public event EventHandler? GameOpened;

    public event EventHandler? GameStopped;

    #endregion Public Events

    #region Public Properties

    public ViewMode GameViewMode { get; set; }

    public bool IsGameRunning => _emulator.Running;

    public ColorPalette SelectedColorPalette { get; set; }

    public ControllerSetting SelectedControllerSetting { get; set; }

    #endregion Public Properties

    #region Public Methods

    public void OpenGame(string fileName)
    {
        _emulator.Open(fileName);
        _gameThread = new Thread(EmulatorRunProc)
        {
            IsBackground = true
        };
        _gameThread.Start();
        GameOpened?.Invoke(this, EventArgs.Empty);
    }

    public void ResetGame()
    {
        _emulator.Reset();
    }

    public void StopGame()
    {
        _emulator.Stop();
        if (_gameThread?.ThreadState == ThreadState.Running) _gameThread.Join();
        GameStopped?.Invoke(this, EventArgs.Empty);
    }

    #endregion Public Methods

    #region Protected Methods

    protected override void Draw()
    {
        if (!_emulator.Running || _texture == null || _drawFrameEventArgs == null)
        {
            Editor.spriteBatch.Begin();
            Editor.GraphicsDevice.Clear(Color.Black);
            Editor.spriteBatch.DrawString(_captionFont, CaptionText,
                new Vector2((Width - _captionTextSize.X) / 2, (Height - _captionTextSize.Y) / 2), TextColors);
            Editor.spriteBatch.End();
            return;
        }

        var colorBytes = _drawFrameEventArgs.BitmapData;

        Parallel.For(0, 256 * 240, i => { _colors[i] = SelectedColorPalette.Colors[colorBytes[i]]; });

        Rectangle drawRect;
        int w, h;
        float factor;
        switch (GameViewMode)
        {
            case ViewMode.FullWindow:
                drawRect = new Rectangle(0, 0, Width, Height);
                _backgroundColor = Color.Black;
                break;

            case ViewMode.Normal:
                drawRect = new Rectangle((Width - 256) / 2, (Height - 240) / 2, 256, 240);
                _backgroundColor = Color.Black;
                break;

            case ViewMode.Scaled:
                factor = Math.Min(Width / 256f, Height / 240f);
                w = (int)Math.Floor(256 * factor);
                h = (int)Math.Floor(240 * factor);
                drawRect = new Rectangle((Width - w) / 2, (Height - h) / 2, w, h);
                _backgroundColor = Color.Black;
                break;

            case ViewMode.Debug:
                factor = Math.Min(Width / 256f, Height / 240f);
                w = (int)Math.Floor(256 * factor);
                h = (int)Math.Floor(240 * factor);
                drawRect = new Rectangle(0, 0, w, h);
                _backgroundColor = Color.Blue;
                break;

            default:
                drawRect = new Rectangle(0, 0, Width, Height);
                _backgroundColor = Color.Black;
                break;
        }

        _texture.SetData(_colors);
        Editor.spriteBatch.Begin();
        Editor.GraphicsDevice.Clear(_backgroundColor);
        Editor.spriteBatch.Draw(_texture, drawRect, Color.White);
        if (GameViewMode == ViewMode.Debug)
        {
            DrawDebugInformation(Editor.spriteBatch, drawRect.X + drawRect.Width + 10, 10);
        }

        Editor.spriteBatch.End();
    }

    protected override void Initialize()
    {
        _captionFontSystem.AddFont(File.ReadAllBytes(@"c:\windows\fonts\times.ttf"));
        _captionFont = _captionFontSystem.GetFont(30);

        _dbgFontSystem.AddFont(File.ReadAllBytes(@"c:\windows\fonts\consolab.ttf"));
        _dbgFont = _dbgFontSystem.GetFont(26);
        _dbgFontHeight = (int)Math.Ceiling(_dbgFont.MeasureString("TEST").Y) + 5;

        _texture = new Texture2D(Editor.GraphicsDevice, 256, 240);
        if (Editor.FPSCounter != null)
        {
            Editor.FPSCounter.Visible = false;
        }

        _captionTextSize = _captionFont.MeasureString(CaptionText);
    }

    protected override void Update(GameTime gameTime)
    {
        if (_emulator.Running)
        {
            if (_input.IsKeyPressed(SelectedControllerSetting.Up))
                _emulator.Controller.SetButtonState(Controller.Buttons.Up, true);
            if (_input.IsKeyReleased(SelectedControllerSetting.Up))
                _emulator.Controller.SetButtonState(Controller.Buttons.Up, false);

            if (_input.IsKeyPressed(SelectedControllerSetting.Down))
                _emulator.Controller.SetButtonState(Controller.Buttons.Down, true);
            if (_input.IsKeyReleased(SelectedControllerSetting.Down))
                _emulator.Controller.SetButtonState(Controller.Buttons.Down, false);

            if (_input.IsKeyPressed(SelectedControllerSetting.Left))
                _emulator.Controller.SetButtonState(Controller.Buttons.Left, true);
            if (_input.IsKeyReleased(SelectedControllerSetting.Left))
                _emulator.Controller.SetButtonState(Controller.Buttons.Left, false);

            if (_input.IsKeyPressed(SelectedControllerSetting.Right))
                _emulator.Controller.SetButtonState(Controller.Buttons.Right, true);
            if (_input.IsKeyReleased(SelectedControllerSetting.Right))
                _emulator.Controller.SetButtonState(Controller.Buttons.Right, false);

            if (_input.IsKeyPressed(SelectedControllerSetting.B))
                _emulator.Controller.SetButtonState(Controller.Buttons.B, true);
            if (_input.IsKeyReleased(SelectedControllerSetting.B))
                _emulator.Controller.SetButtonState(Controller.Buttons.B, false);

            if (_input.IsKeyPressed(SelectedControllerSetting.A))
                _emulator.Controller.SetButtonState(Controller.Buttons.A, true);
            if (_input.IsKeyReleased(SelectedControllerSetting.A))
                _emulator.Controller.SetButtonState(Controller.Buttons.A, false);

            if (_input.IsKeyPressed(SelectedControllerSetting.Select))
                _emulator.Controller.SetButtonState(Controller.Buttons.Select, true);
            if (_input.IsKeyReleased(SelectedControllerSetting.Select))
                _emulator.Controller.SetButtonState(Controller.Buttons.Select, false);

            if (_input.IsKeyPressed(SelectedControllerSetting.Start))
                _emulator.Controller.SetButtonState(Controller.Buttons.Start, true);
            if (_input.IsKeyReleased(SelectedControllerSetting.Start))
                _emulator.Controller.SetButtonState(Controller.Buttons.Start, false);
        }
    }

    #endregion Protected Methods

    #region Private Methods

    private void _emulator_CpuStepped(object? sender, CpuStepEventArgs e)
    {
        if (GameViewMode == ViewMode.Debug && _instructionQueue.Count < 100)
        {
            _instructionQueue.Enqueue(e.DisassembledInstruction);
        }
    }

    private void _emulator_DrawFrame(object? sender, DrawFrameEventArgs e)
    {
        _drawFrameEventArgs = e;
    }

    private void DrawDebugInformation(SpriteBatch spriteBatch, int x, int y)
    {
        if (_drawFrameEventArgs is null) return;

        // Write CPU Registers
        spriteBatch.DrawString(_dbgFont, "CPU Registers:", new Vector2(x, y), Color.White);
        var cpuRegisterInfoLine =
            $"PC=${_drawFrameEventArgs.PC:X4} A=${_drawFrameEventArgs.A:X2} X=${_drawFrameEventArgs.X:X2} Y=${_drawFrameEventArgs.Y:X2} SP=${_drawFrameEventArgs.SP:X2}";
        spriteBatch.DrawString(_dbgFont, cpuRegisterInfoLine, new Vector2(x + 15, y + _dbgFontHeight), Color.White);

        // Write CPU Flags
        spriteBatch.DrawString(_dbgFont, "CPU Flags:", new Vector2(x, y + _dbgFontHeight * 3), Color.White);
        spriteBatch.DrawString(_dbgFont, "N", new Vector2(x + 15, y + _dbgFontHeight * 4),
            _drawFrameEventArgs.CpuFlags.N ? Color.LightGreen : Color.Red);
        spriteBatch.DrawString(_dbgFont, "V", new Vector2(x + 35, y + _dbgFontHeight * 4),
            _drawFrameEventArgs.CpuFlags.V ? Color.LightGreen : Color.Red);
        spriteBatch.DrawString(_dbgFont, "-", new Vector2(x + 55, y + _dbgFontHeight * 4),
            _drawFrameEventArgs.CpuFlags.Bit5 ? Color.LightGreen : Color.Red);
        spriteBatch.DrawString(_dbgFont, "B", new Vector2(x + 75, y + _dbgFontHeight * 4),
            _drawFrameEventArgs.CpuFlags.Bit4 ? Color.LightGreen : Color.Red);
        spriteBatch.DrawString(_dbgFont, "D", new Vector2(x + 95, y + _dbgFontHeight * 4),
            _drawFrameEventArgs.CpuFlags.D ? Color.LightGreen : Color.Red);
        spriteBatch.DrawString(_dbgFont, "I", new Vector2(x + 115, y + _dbgFontHeight * 4),
            _drawFrameEventArgs.CpuFlags.I ? Color.LightGreen : Color.Red);
        spriteBatch.DrawString(_dbgFont, "Z", new Vector2(x + 135, y + _dbgFontHeight * 4),
            _drawFrameEventArgs.CpuFlags.Z ? Color.LightGreen : Color.Red);
        spriteBatch.DrawString(_dbgFont, "C", new Vector2(x + 155, y + _dbgFontHeight * 4),
            _drawFrameEventArgs.CpuFlags.C ? Color.LightGreen : Color.Red);

        spriteBatch.DrawString(_dbgFont, "PPU Registers:", new Vector2(x, y + _dbgFontHeight * 6), Color.White);
        spriteBatch.DrawString(_dbgFont,
            $"PPUADDR=${_drawFrameEventArgs.PpuAddress:X4} PPUCTRL=${_drawFrameEventArgs.PpuControl:X2}",
            new Vector2(x + 15, y + _dbgFontHeight * 7), Color.White);
        spriteBatch.DrawString(_dbgFont,
            $"PPUMASK=${_drawFrameEventArgs.PpuMask:X2}   PPUSTAT=${_drawFrameEventArgs.PpuStatus:X2}",
            new Vector2(x + 15, y + _dbgFontHeight * 8), Color.White);
        spriteBatch.DrawString(_dbgFont,
            $"PPUSCRL=${_drawFrameEventArgs.PpuScroll:X2}",
            new Vector2(x + 15, y + _dbgFontHeight * 9), Color.White);

        if (_instructionQueue.Count > 0)
        {
            spriteBatch.DrawString(_dbgFont, "Disassembly:", new Vector2(x, y + _dbgFontHeight * 11), Color.White);
            var curIdx = 0;
            while (curIdx < 25)
            {
                var inst = _instructionQueue.Dequeue();
                spriteBatch.DrawString(_dbgFont, inst, new Vector2(x + 15, y + _dbgFontHeight * (12 + curIdx)),
                    Color.White);
                curIdx++;
            }
        }
    }

    private void EmulatorRunProc()
    {
        try
        {
            _emulator.Run();
        }
        catch (Exception e)
        {
            var msg = new StringBuilder();
            msg.AppendLine("General error occurred during the game execution.");
            msg.AppendLine(e.Message);
            MessageBox.Show(msg.ToString(), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
            StopGame();
        }
    }

    #endregion Private Methods
}